using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Runtime.InteropServices;
using System.Reflection;

using System.Net;

using System.Diagnostics;

using SHDocVw;


namespace IEeee
{
    /// <summary>
    /// A class that represents a snapshot of the state of the web browser.
    /// The information included in this snapshot includes:
    /// <list type="bullet">
    ///     <item>
    ///         <description>Details of the browser</description>
    ///     </item>
    ///     <item>
    ///         <description>Details of the computer's screen</description>
    ///     </item>
    ///     <item>
    ///         <description>The source code of the current page</description>
    ///     </item>
    ///     <item>
    ///         <description>The source code of any CSS files referenced 
    ///                      in the current page</description>
    ///     </item>
    ///     <item>
    ///         <description>Details of and the source code for any Frames or 
    ///                      IFrames in the current page</description>
    ///     </item>
    ///     <item>
    ///         <description>Details (and if availiable) the source code for any 
    ///                      script files referenced in the current page.</description>
    ///     </item>
    /// </list>
    /// </summary>
    [ComVisible(false)]
    class IEStateShapshot : IDisposable
    {

        //    
        // A list containing all the files that have been written as part of this snapshot
        //
        private List<AttachmentDetails> m_AttachmentDetails = new List<AttachmentDetails> ();

        //
        // A summary of the snapshot
        //
//        private String m_Description;

        private List<SnapShotSection> m_Sections = new List<SnapShotSection> ();

        // 
        // The Url of the current page when the snapshot was taken.
        //
        private String m_Url;

        private String m_TempDirName;

        private IEStateShapshot (String tempDirName)
        {
            ArgTest.IsNotNullOrEmpty (tempDirName, "tempDirName");
            m_TempDirName = tempDirName;
        }

        ~IEStateShapshot()
        {
            Debug.Assert(m_AttachmentDetails == null, "IEStateShapshot object was not Dispossed");
        }
        
        /// <summary>
        /// Deletes all the files that have been created for this snapshot.
        /// </summary>
        public void Dispose ()
        {
            if (m_AttachmentDetails != null)
            {
                foreach (AttachmentDetails details in m_AttachmentDetails)
                {
                    details.Dispose ();
                }
                m_AttachmentDetails = null;
            }
            m_Url = null;
            m_Sections = null;

            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// The URL of the page that the browser had loaded 
        /// when the snap shot was taken.
        /// </summary>
        public String Url
        {
            get { return m_Url; }
            private set { 
                ArgTest.IsNotNullOrEmpty (value, "value");
                m_Url = value; 
            }
        }

        /// <summary>
        /// The list of attached files that are contained in
        /// this snapshot.
        /// </summary>
        internal List<AttachmentDetails> Attachments
        {
            get { return m_AttachmentDetails; }
        }

        /// <summary>
        /// A textual description of the state of the browser
        /// when the snapshot was taken.
        /// </summary>
        public List<SnapShotSection> Sections
        {
            get { return m_Sections; }
        }

        public String GetDescription ()
        {
            StringBuilder buf = new StringBuilder ();

            foreach (SnapShotSection section in m_Sections)
            {
                buf.Append (section.ToString ());
                buf.Append ("\n\n");
            }

            return buf.ToString ();
        }

        private static String GetFileExtensionFromMimeType (String mimeType)
        {
            ArgTest.IsNotNullOrEmpty (mimeType, "mimeType");

            String [] splitString = mimeType.Split (new Char[] { '/' });
            if (splitString.Length != 3)
            {
                throw new Exception ("Error parsing mime type \"" + mimeType + "\"");
            }

            return splitString[2];
        }

        /// <summary>
        /// Creates a snapshot of the current state of the passed HtmlDocument.
        /// </summary>
        /// <param name="doc">The document to inspect</param>
        /// <returns>The Snapshot object that represents the state.</returns>
        public static IEStateShapshot TakeSnapshot (String tempDirName, mshtml.IHTMLDocument2 doc)
        {
            ArgTest.IsNotNull (doc, "doc");
            ArgTest.IsNotNullOrEmpty (tempDirName, "tempDirName");

            IEStateShapshot snapShot = new IEStateShapshot (tempDirName);

            // Need to reset the counter that we use to keep att the attachment
            // filenames unique.
            //
            AttachmentDetails.ResetWorkItemCounter ();

            // 1st off grab the source for the current page.
            //
            AttachmentDetails mainAttchment = new AttachmentDetails (tempDirName,
                                                                     doc.location.toString (),
                                                                     AttachmentType.Document,
                                                                     "txt"/*GetFileExtensionFromMimeType (doc.mimeType)*/,
                                                                     doc.body.outerHTML);
            snapShot.Attachments.Add(mainAttchment);

            snapShot.Url = doc.url;
            

            // Now collect loads of information about the browser and the
            // page it has open.
            //
            snapShot.m_Sections.Add (snapShot.GetDocumentDetails (doc));

            snapShot.m_Sections.Add (snapShot.GetNavigatorDetails (doc.parentWindow.navigator));

            snapShot.m_Sections.Add (snapShot.GetScreenDetails (doc.parentWindow.screen));

            snapShot.m_Sections.Add (snapShot.GetStyleSheetDetails (doc));

            snapShot.m_Sections.Add (snapShot.GetScriptDetails (doc));

            // Finally get all of the Frames and IFrames in the page
            //
            SnapShotSection framesAndIFramesSection = new SnapShotSection ("Frames & IFrames");

            snapShot.GetFrameDetails (framesAndIFramesSection, doc);

            return snapShot;
        }


        /// <summary>
        /// Appends the passed title to the passed StringBuilder in
        /// the standard format - currently this is underlined.
        /// </summary>
        private static void AppendTitle (StringBuilder buf, String title)
        {
            buf.Append ("\n\n");
            buf.Append (title);
            buf.Append ("\n");
            buf.Append ('-', title.Length);
        }

        /// <summary>
        /// Appends the passed name and value to the passed StringBuilder in
        /// the standard format. If value is null, nothing is written.
        /// </summary>
        private static void AppendNameValue (StringBuilder buf, String name, String value)
        {
            Debug.Assert (name != null);
            Debug.Assert (buf != null);

            if (String.IsNullOrEmpty (value) == false)
            {
                buf.Append ("\n  ");
                buf.Append (name);
                buf.Append (": ");
                buf.Append (value);
            }
        }

        /// <summary>
        /// Returns a String containing details about the computers screen.
        /// </summary>
        private SnapShotSection GetScreenDetails (mshtml.IHTMLScreen screen)
        {
            Debug.Assert (screen != null);

            SnapShotSection section = new SnapShotSection ("Screen Details");
            if (screen == null)
            {
                section.AddAttribute ("Error", "Unable to get Screen details");
            }
            else
            {

                try
                {
                    section.AddAttribute ("Height", screen.height);
                    section.AddAttribute ("Width", screen.width);
                    section.AddAttribute ("Color Depth", screen.colorDepth);

                    // Ignore this if the value is 0
                    if (screen.bufferDepth > 0)
                    {
                        section.AddAttribute ("Buffer Depth", screen.bufferDepth);
                    }

                    // Ignore this if the value is 0
                    if (screen.updateInterval > 0)
                    {
                        section.AddAttribute ("Update Inteval", screen.updateInterval);
                    }

                    section.AddAttribute ("Smooth fonts enabled", screen.fontSmoothingEnabled);
                    section.AddAttribute ("Availiable Height", screen.availHeight);
                    section.AddAttribute ("Availiable Width", screen.availWidth);
                }
                catch (COMException ex)
                {
                    // Not particually nice, but any errors that occure need 
                    // to be reported. 
                    section.AddAttribute ("Error", ex.Message);
                    Debug.WriteLine (ex.ToString ());
                }
            }

            return section;
        }

        /// <summary>
        /// Returns a String containing details of the passed document.
        /// </summary>
        private SnapShotSection GetDocumentDetails (mshtml.IHTMLDocument2 doc)
        {
            Debug.Assert (doc != null);

            SnapShotSection section = new SnapShotSection ("Document Details");
            if (doc == null)
            {
                section.AddAttribute ("Error", "Unable to get Document details");
            }
            else
            {
                try
                {
                    section.AddAttribute ("URL", doc.url);

                    section.AddAttribute ("Title", doc.title);

                    section.AddAttribute ("Default Char Set", doc.defaultCharset);

                    // This throws an E_FAIL if there is no domain,
                    // so have to wrap it in an simple handler
                    try
                    {
                        // This exception gets caught by the debugger and that is really anoying me,
                        // so as a fix, don't make the call if the debugger is attached.
                        //
                        if (System.Diagnostics.Debugger.IsAttached == false)
                        {
                            section.AddAttribute("Security Domain", doc.domain);
                        }
                    }
                    catch (COMException) { }

                    //buffer.Append ("\n  MIME Type: ");
                    //buffer.Append (doc.mimeType);

                    section.AddAttribute ("Referrer", doc.referrer);

                    section.AddAttribute ("Security Info", doc.security);
                }
                catch (COMException ex)
                {
                    // Not particually nice, but any errors that occur need 
                    // to be reported. 
                    section.AddAttribute ("Error", ex.Message);
                    Debug.WriteLine (ex.ToString ());
                }
            }

            return section;
        }

        /// <summary>
        /// Not currently used.
        /// </summary>
//        private String GetEventDetails (mshtml.IHTMLEventObj evt)
//        {
//            Debug.Assert (evt != null);

//            StringBuilder buffer = new StringBuilder ();
//            if (evt == null)
//            {
//                buffer.Append ("\nUnable to get Event details\n");
//            }
//            else
//            {
//                AppendTitle (buffer, "Last Event Details");

////                section.AddAttribute ("Event Type", evt.type);

//                //              section.AddAttribute ("Source Element", evt.srcElement);

//                //              section.AddAttribute ("Key Code", evt.keyCode);


//            }

//            return buffer.ToString ();
//        }

        /// <summary>
        /// Returns a String containing details about the web browser.
        /// </summary>
        private SnapShotSection GetNavigatorDetails (mshtml.IOmNavigator nav)
        {
            Debug.Assert (nav != null);

            SnapShotSection section = new SnapShotSection ("Browser Details");
            if (nav == null)
            {
                section.AddAttribute ("Error", "Unable to get Browser details");
            }
            else
            {
                try
                {
                    section.AddAttribute ("Browser Name", nav.appName);

                    section.AddAttribute ("Browser Version", nav.appVersion);

                    section.AddAttribute ("Operating System", nav.platform);

                    section.AddAttribute ("Cookies Enabled", nav.cookieEnabled.ToString ());

                    section.AddAttribute ("User Agent", nav.userAgent);

                    section.AddAttribute ("User Language", nav.userLanguage);
                }
                catch (COMException ex)
                {
                    // Not particually nice, but any errors that occure need 
                    // to be reported. 
                    section.AddAttribute ("Error", ex.Message);
                    Debug.WriteLine (ex.ToString ());
                }
            }

            return section;
        }


        /// <summary>
        /// Not currently used.
        /// </summary>
        //private String GetHistoryDetails (mshtml.IOmHistory history)
        //{
        //    Debug.Assert (history != null);

        //    StringBuilder buffer = new StringBuilder ();
        //    if (history == null)
        //    {
        //        buffer.Append ("\nUnable to get History details\n");
        //    }
        //    else
        //    {
        //        AppendTitle (buffer, "History Details");

        //    }

        //    return buffer.ToString ();
        //}

        /// <summary>
        /// Returns a string containing details about all the script files that
        /// are referenced by passed document.
        /// If the source code is available for the script, the script will
        /// be included in the AttachmentDetails collection.
        /// </summary>
        private SnapShotSection GetScriptDetails (mshtml.IHTMLDocument2 doc)
        {
            Debug.Assert (doc != null);

            SnapShotSection section = new SnapShotSection ("Script Files");
            if (doc == null)
            {
                section.AddAttribute ("Error", "Unable to get Script details");
            }
            else
            {
                int count = 1;
                foreach (mshtml.IHTMLScriptElement script in doc.scripts)
                {
                    try
                    {
                        if (String.IsNullOrEmpty (script.src) == false)
                        {
                            section.AddAttribute (count.ToString (), script.src);
                            count++;

                            //
                            // Currently this doesn't work as the contents of any script files
                            // doesn't seem to be kept.
                            // To soulve this, I need to write this to use the WinInet to 
                            // request the contents of the script's src property.
                            // Hopefully by using WinInet, any credentials that the browser
                            // has with the site will be used on the GET of the script.
                            //
                            if (String.IsNullOrEmpty (script.text) == true)
                            {
                                StringBuilder header = new StringBuilder ();
                                if (String.IsNullOrEmpty (script.@event) == false)
                                {
                                    header.Append ("\n    Event: ");
                                    header.Append (script.@event);
                                }
                                if (String.IsNullOrEmpty (script.htmlFor) == false)
                                {
                                    header.Append ("\n    HTML For: ");
                                    header.Append (script.htmlFor);
                                }

                                Uri scriptUri = GetAbsoluteUriForDepedantDocument (script.src, doc);
                                
                                String scriptBody = HttpRequestSession.GetBody (scriptUri, doc.parentWindow.navigator.userAgent);

                                String fileExtension = "txt"; // need a sensible default

                                // It might be useful in the future to use the file extension, but
                                // at the moment, when you open an attachment from VS, it does a 
                                // ShellExecute on the file - this means it tries to execute the 
                                // script rather than open it in an editor.
                                //

                                //String scriptPath = scriptUri.GetLeftPart (UriPartial.Path);
                                //if (String.IsNullOrEmpty (scriptPath) == false)
                                //{
                                //    int dotIndex = scriptPath.LastIndexOf ('.');
                                //    if (dotIndex != -1)
                                //    {
                                //        dotIndex += 1;
                                //        String parsedExtension = scriptPath.Substring (dotIndex, scriptPath.Length - dotIndex);

                                //        if (String.IsNullOrEmpty (parsedExtension) == false)
                                //        {
                                //            fileExtension = parsedExtension;
                                //        }
                                //    }
                                //}

                                AttachmentDetails attachment = new AttachmentDetails (m_TempDirName,
                                                                                      scriptUri.AbsoluteUri,
                                                                                      AttachmentType.Script,
                                                                                      fileExtension,
                                                                                      scriptBody,
                                                                                      header.ToString ());
                                m_AttachmentDetails.Add (attachment);

                            }
                            else
                            {
                                //                                section.AddAttribute (count.ToString (), "source code not available");
                                //                                count++;
                                // This will be an inline script block.
                            }
                        }

                    }

                    catch (Exception ex)
                    {
                        // Not particually nice, but any errors that occure need 
                        // to be reported. 
                        section.AddAttribute ("Error", ex.Message);
                        Debug.WriteLine (ex.ToString ());
                    }

                }
            }
            return section;

        }

        /// <summary>
        /// Returns a String containing details of the Frames and IFrames in the passed document.
        /// The source code for these is also added to the AttachmentDetails collection.
        /// </summary>
        private void GetFrameDetails (SnapShotSection section, mshtml.IHTMLDocument2 doc)
        {
            GetFrameDetails (section, doc, 2);
        }

        /// <summary>
        /// Returns a String containing details of the Frames and IFrames in the passed document.
        /// The source code for these is also added to the AttachmentDetails collection.
        /// Any frames identified are indented by <paramref name="indent"/> number of spaces.
        /// </summary>
        private void GetFrameDetails (SnapShotSection section, mshtml.IHTMLDocument2 doc, int indent)
        {
            Debug.Assert (doc != null);
            String indentString = new String (' ', indent);

            if (doc == null)
            {
                section.AddAttribute ("Error", "Unable to get Frame details");
            }
            else
            {
                for (int i = 0; i < doc.frames.length; i++)
                {
                    object index = i;
                    mshtml.IHTMLWindow2 frame = (mshtml.IHTMLWindow2) doc.frames.item (ref index);

                    try
                    {
                        section.AddAttribute (indentString, frame.location.toString ());

                        SnapShotSection docDetails = GetDocumentDetails (frame.document);


                        AttachmentDetails attachment = new AttachmentDetails (m_TempDirName,
                                                                              frame.document.location.toString (),
                                                                              AttachmentType.Frame,
                                                                              "txt",
                                                                              frame.document.body.outerHTML.ToString (),
                                                                              docDetails.ToString ());
                        m_AttachmentDetails.Add (attachment);

                        if (frame.document != null && frame.document.frames != null && frame.document.frames.length > 0)
                        {
                            GetFrameDetails (section, frame.document, indent + 2);
                        }
                    }
                    catch (UnauthorizedAccessException)
                    {
                        // I have seen this get thrown on one page, so we catch it and 
                        // put up a less than useful description.
                        section.AddAttribute (indentString, "Unable to retrieve frame location");
                    }
                }
            }
        }

        /// <summary>
        /// Returns a String conatining a textual descripion of all the StyleSheets that are
        /// referenced in the passed document. This is recuresive, so any @include'ed stylesheets
        /// will also be details. The source code for these files is added to the
        /// AttachmentDetails collection
        /// </summary>
        private SnapShotSection GetStyleSheetDetails (mshtml.IHTMLDocument2 doc)
        {
            Debug.Assert (doc != null);

            SnapShotSection section = new SnapShotSection ("Referenced StyleSheets");
            if (doc == null)
            {
                section.AddAttribute ("Error", "\nUnable to get Stylesheet details\n");
            }
            else
            {
                AppendStyleSheetDetails (section, doc.styleSheets);
            }

            return section;
        }

        /// <summary>
        /// Recursivley looks at all the stylesheets, listing them and attaching their source.
        /// </summary>
        private void AppendStyleSheetDetails (SnapShotSection section, mshtml.IHTMLStyleSheetsCollection sheets)
        {
            int count = 1;
            foreach (mshtml.IHTMLStyleSheet sheet in sheets)
            {

                if (String.IsNullOrEmpty (sheet.href) == false)
                {

                    section.AddAttribute (count.ToString (), sheet.href);
                    count++;

                    String contents = sheet.cssText;
                    if (String.IsNullOrEmpty (contents)) {
                        contents = "This stylesheet is empty\n\nA posible cause for this might be that the reference to the style sheet is incorrect";
                    }

                    AttachmentDetails attachment = new AttachmentDetails (m_TempDirName, 
                                                                          sheet.href,
                                                                          AttachmentType.StyleSheet,
                                                                          "css",
                                                                          contents);
                    m_AttachmentDetails.Add (attachment);
                }
                else
                {
                    section.AddAttribute (count.ToString (), "Unknown Sheet");
                    count++;
                }
                if (sheet.imports.length > 0)
                {
                    AppendStyleSheetDetails (section, sheet.imports);
                }
            }
        }

        /// <summary>
        /// Returns an absolute Uri for a document that may be part of a parent doc.
        /// e.g. if we see &lt;script src="/myscript.js" in www.mysite.com then this 
        /// function will generate a Uri of http://www.mysite.com/myscript.js
        /// </summary>
        /// <param name="dependantDoc">The Uri of the referenced document</param>
        /// <param name="parentDoc">The document that it is referenced from</param>
        /// <returns>An absolute Uri for the dependant doc.</returns>
        private Uri GetAbsoluteUriForDepedantDocument (String dependantDoc, mshtml.IHTMLDocument2 parentDoc)
        {
            ArgTest.IsNotNullOrEmpty (dependantDoc, "dependantDoc");
            ArgTest.IsNotNull (parentDoc, "parentDoc");

            Uri retVal = null;
            if (Uri.IsWellFormedUriString (dependantDoc, UriKind.Absolute))
            {
                retVal = new Uri (dependantDoc);
            }
            else
            {
                retVal = new Uri (new Uri (parentDoc.location.href), dependantDoc);
            }
            return retVal;
        }

        /// <summary>
        /// Returns a file name that can be used to save the contents of the passed
        /// address.
        /// </summary>
//        private static String GetFileNameForUri (String address)
//        {
//            return GetFileNameForUri (new Uri (address));
//        }

        /// <summary>
        /// Returns a file name that can be used to save the contents of the passed
        /// address.
        /// </summary>
//        private static String GetFileNameForUri (Uri address)
//        {
//            return GetFileNameForPrefixAndPath (address.Host, address.AbsolutePath);
//        }

        /// <summary>
        /// Returns a file name that can be used to save the contents of the passed
        /// address.
        /// </summary>
        //private static String GetFileNameForPrefixAndPath (String prefix, String path)
        //{
        //    StringBuilder fileName = new StringBuilder ();

        //    prefix = prefix.Replace ('/', '-');
        //    prefix = prefix.Replace ('\\', '-');

        //    fileName.Append ("[");
        //    fileName.Append (prefix);
        //    fileName.Append ("] ");

        //    if (String.IsNullOrEmpty (path) || path.Equals ("/") || path.Equals ("\\"))
        //    {
        //        path = "Root Home Page";
        //    }
        //    else
        //    {
        //        if (path.StartsWith ("/") || path.StartsWith ("\\"))
        //        {
        //            path = path.Remove (0, 1);
        //        }

        //        path = path.Replace ('/', '_');
        //        path = path.Replace ('\\', '_');
        //    }

        //    fileName.Append (path);

        //    return fileName.ToString ();
        //}

    }
}
